Skip to content

feat(runtime): set values for unordered collections and tensor quantities of any rank - #121

Merged
HuiJun merged 20 commits into
mainfrom
feature/set-and-tensor-values
Sep 8, 2026
Merged

feat(runtime): set values for unordered collections and tensor quantities of any rank#121
HuiJun merged 20 commits into
mainfrom
feature/set-and-tensor-values

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What and why

The Kernel Data Type Library declares some collections' elements unique and unordered, but the runtime read every multi-valued feature as an ordered sequence: a Set { :>> elements = (1, 2, 2, 3); } answered [1, 2, 2, 3], CollectionFunctions::size(s) answered 1 (it counted the object) and two sets written in different orders were unequal. Tensor quantities of rank three and above constructed, but nothing pinned their indexing, shape checks, wire form or native/RDF behaviour.

Set values. Set::elements, UniqueCollection::elements and Map::elements — the features Collections.kerml declares unique without ordered — now hold a ValSet (internal/core/runtime/set_feature.go). Classification follows the library declaration through specialization and redefinition (Context.holdsSet), so a user attribute def MySet :> Set holds a set and a Bag, List, Array, OrderedSet or OrderedMap, or any feature declared ordered/nonunique, stays a sequence. A set keeps each member once, size counts members, ==/!=/same ignore the order written, contains/containsAll are membership, and isEmpty reflects the members.

Every operation whose library signature is ordered nonunique is unchanged; a set reaching one is read as its members in a canonical order. That order is total and documented (set_order.go, docs/reference/wire-contract.md, spec-compliance.md): nulls, then false before true, numbers ascending, complex by real then imaginary, strings lexicographic, quantities by dimension then magnitude, enumeration literals by declaration (qualified name, then document and position, so same-named literals of different enumerations still order alike), objects by identity, everything else by trace text and then by its elements — only valueEqual values compare as neither before nor after. collect, select, head, tail, #, comparison against a sequence, trace rendering and a write into an ordered or nonunique feature all consume that enumeration. FormatTraceValue already rendered Set{…}; it now renders the canonical enumeration rather than insertion order, which is the only change to existing expectations:

  • internal/core/runtime/value_test.go TestSetElementsPreserveInsertionOrderTestSetElementsEnumerateInCanonicalOrder: {2, 1, 2, 3} enumerates [1, 2, 3], not [2, 1, 3].
  • internal/repl/runtime_commands_test.go TestFormatValue/set: Set{"a", "z"}, not Set{"z", "a"}.

Both follow from Collections.kerml's Set ("unique and unordered"): an unordered value has no insertion order to preserve, and a deterministic enumeration is what makes equal sets print and index alike. No existing conformance golden or trace changed; every set fixture is new.

There is no distinct in the bundled library, and SequenceFunctions::union/intersection/including/includingAt/excluding are all declared ordered nonunique (SequenceFunctions.kerml), so they remain sequence-valued even over set operands; library_set_sequence_functions.sysml pins that.

Tensors of any rank. TensorQuantity is rank-agnostic: a TensorMeasurementReference with :>> dimensions = (2, 2, 2) builds a rank-three tensor, # takes one index per dimension, and each shape failure is a typed error — too few/too many indexes and a component count off flattenedSize are ErrMultiplicityViolation, an index outside 1..dimension is ErrIndexOutOfRange, a non-Integer index is ErrTypeMismatch, and +/- between different shapes is ErrMultiplicityViolation naming both. Shape survives +, - and the scalar products; formatting is Tensor(2, 2, 2)[…] [m].

gRPC. api/proto/sysml.proto gains two Value arms:

ValueSet set = 18;                 // message ValueSet { repeated Value elements = 1; }
TensorQuantity tensor_quantity = 19; // message TensorQuantity { repeated int64 dimensions = 1; repeated Quantity components = 2; }

A set is sent in canonical order and accepted in any order; a repeated member is INVALID_ARGUMENT. A tensor is its positive dimensions and one scalar Quantity per row-major component; a non-positive dimension, a component count off the product or a component without a magnitude is INVALID_ARGUMENT. A rank-one tensor stays a tensor, distinct from VectorQuantity. The service advertises set_values and tensor_values; a client that omits either receives the unsupported null it always did and an inbound set/tensor is UNIMPLEMENTED — nested values are inspected recursively. Stubs were regenerated with make proto proto-buf python-proto proto-ts proto-rust; the Python gRPC header pin (GRPC_GENERATED_VERSION = '1.83.0') matches CI's grpcio-tools. The Go, Python, Node, Rust and Java clients each gain a native set type (unique, order-insensitive equality, nestable) and tensor type (positive dimensions, exact shape, row-major get, wrong-rank and out-of-range errors, shared unit when there is one), decode both arms, send both as calc arguments, and refuse them locally to a service without the capability. internal/grpc/convert_structured_test.go TestTensorQuantityCrossesAsUnsupported is removed: it asserted the arm did not exist, and TestTensorQuantityRoundTrip / TestSetAndTensorCapabilities replace it with the arm present and the arm withheld.

RDF. The mapping represents models, not evaluated values, so neither value has a literal form; a set-valued or tensor-valued feature exports as the expression tree that values it and reads back verbatim. rdf_expr.go now spells a multi-index as cube#(2, 1, 2) rather than cube#((2, 1, 2)). Documented in docs/reference/rdf-mapping.md.

Native compilation. A calc with a set or tensor parameter, result, attribute or local is refused with codegen.UnsupportedError (Unwrap() == ErrUnsupported) naming the type; documented in docs/project/native-compilation.md and spec-compliance.md.

Specification basis

KerML 1.1 Kernel Data Type Library, Collections.kerml: Collection::elements (nonunique), OrderedCollection::elements (ordered nonunique), UniqueCollection::elements (unique, "not necessarily ordered"), Bag ("unordered and nonunique"), Set ("unique and unordered"), OrderedSet ("unique and ordered"), Map/OrderedMap; SequenceFunctions.kerml union, intersection, including, includingAt, excluding (ordered nonunique); CollectionFunctions.kerml ==, size, isEmpty, notEmpty, contains, containsAll. SysML v2 1.1 Quantities.kerml TensorQuantityValue and MeasurementReferences.kerml TensorMeasurementReference::dimensions/flattenedSize.

docs/project/spec-compliance.md gains set and tensor rows (canonical iteration, ordered-boundary conversion, wire encoding, capability behaviour, RDF, native) and its gRPC Values row now covers set/tensor_quantity. Known limitation, recorded there as ⚠️: uniqueness of an ordered unique feature (OrderedSet::elements, OrderedMap::elements, any multi-valued feature not declared nonunique) is still not enforced at runtime — OrderedSet { :>> elements = (1, 1, 2); } reads [1, 1, 2] with size 3, as before this change. The set kind only covers what the library declares unordered.

How it was verified

gofmt -l .        → (empty)
go build ./...    → ok
go vet ./...      → ok
go test ./...     → ok (every package)
make lint         → staticcheck and gosec clean
make docs-check   → 0 broken links, no internal labels, changelog fragments valid

OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 \
  go test -count=1 ./internal/core/model -run 'TestTrainingExamples|TestPilotCorpora'
  → ok  github.com/Open-MBEE/OpenSysML/internal/core/model  15.181s
OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 \
  go test -count=1 -v ./internal/core/export -run TestCorpusRoundTrip
  → corpus round trip: 346 files: 346 stable, 0 whitespace-only, 0 graph-diff, 0 unwritable, 0 unparseable, 0 refused
python3 scripts/changelog.py check → ok

New coverage:

  • Conformance (internal/core/runtime/testdata/conformance/): library_set_elements, library_set_elements_already_distinct, library_set_elements_empty, library_unique_collection_elements, library_map_elements, library_bag_elements, library_ordered_set_elements, library_set_operations (equality regardless of order, membership, size, conversion into ordered/nonunique/plain features), library_set_sequence_functions (every SequenceFunctions result stays a sequence over set operands), calc_set_consumed_by_ordered_operations (+ trace golden), instance_tensor_rank_three, instance_tensor_rank_three_failures.
  • Unit: set_feature_test.go, collections_test.go TestCollectionOperationsOverSets, tensor_test.go (rank-three construction, indexing, arithmetic, equality, formatting).
  • Robustness (robustness_test.go): typed errors for each tensor shape/index failure and higher-rank identity.
  • gRPC: internal/grpc/convert_set_tensor_test.go (round trips, malformed input, nested values on every Value surface, capabilities); client/opensysml/set_tensor_test.go over in-process, Connect protobuf and Connect JSON.
  • RDF: internal/core/export/set_tensor_rdf_test.go.
  • Native: internal/repl/compile_test.go refusals SetParam, SetElements, SetLocal, TensorParam, TensorBuilt.
  • Language-independent conformance: conformance/fixtures/set_tensor.sysml, scenarios in 01-server-info, 04-evaluate, 10-evaluate-calc94 scenarios: 91 passed, 0 failed, 3 skipped against the Go package client.
  • Clients, run against a live sysml-grpc: Python 747 passed (test_set_tensor.py all 54 service-backed and unit cases pass); Node 108 pass; Rust 49 + 5 + 14 passed. The Java sources compile against the regenerated stubs and their tests (PublicTypesTest, ProtosTest, ApiIntegrationTest) are written, but Maven Central rate-limited this machine (HTTP 429 resolving jacoco-maven-plugin) so the Java suite could not be run locally; CI's Java job covers it.

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review September 8, 2026 02:41
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/set-and-tensor-values branch from fec37be to e2a04ef Compare September 8, 2026 04:31
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/set-and-tensor-values branch from e2a04ef to 9bee284 Compare September 8, 2026 05:06
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/set-and-tensor-values branch from 5869152 to a17f385 Compare September 8, 2026 12:52
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/set-and-tensor-values branch from 84aefdc to d2003bb Compare September 8, 2026 14:15
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/set-and-tensor-values branch from 0df8e54 to f0088ad Compare September 8, 2026 19:58
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 9 commits September 8, 2026 21:11
…nsors of any rank

A Collection whose elements the library declares unique and not ordered
(Set, UniqueCollection, Map) holds them as a set: equal regardless of
order, deduplicated, enumerating in a canonical order whenever an ordered
operation consumes it, and converted to that sequence when read into an
ordered or nonunique feature. Collections the library declares ordered
(OrderedSet, OrderedMap, List, Array) or nonunique (Bag) keep their
sequence. CollectionFunctions read a Collection object through its
elements.

Tensor quantities of rank above two construct, index with one index per
dimension, and keep their shape through the existing arithmetic; a wrong
number of indexes or an index outside a dimension is a typed error.

Co-Authored-By: jason.han <[email protected]>
…ire and every client

Value gains dedicated set and tensor_quantity arms, advertised as the set_values and tensor_values capabilities; the Go, Python, Node, Rust and Java clients decode, send and refuse them by capability. Sets have no RDF literal form and neither value compiles natively; both are documented, and the spec-compliance rows cite the library declarations.

Co-Authored-By: jason.han <[email protected]>
…, clients refuse repeated set members

canonicalLess is now a three-way total order: same-rendering values fall back
to declaration identity (enumeration literals, variants) or to their elements
(sequences, sets, arrays), so equal sets enumerate alike whatever order their
members were inserted in. A Set and a Sequence that valueEqual accepts as equal
now receive the same valueKeyFunc key, so they share a bucket and deduplicate.
The Go, Python and Node clients validate an incoming set's uniqueness by their
own value equality — nested sets, sequences and quantities included — as the
Rust and Java clients already did.

Co-Authored-By: jason.han <[email protected]>
canonicalCompare falls through to the contents of every structured kind
whose trace text can coincide — array shape and elements, vector and
tensor components, quantity unit, measurement reference, frame and
transformation keys, expression span — so two unequal values never share
a position and equal sets enumerate alike whatever their insertion order.

Co-Authored-By: jason.han <[email protected]>
…numbers exactly, refuse overflowing tensor shapes

Co-Authored-By: jason.han <[email protected]>
…able units

A set listing a member twice, by each client's value equality, is now refused before it is sent (Go, Node) or on construction and decoding (Python, Rust, Java), so a malformed set never compares equal to a valid one. Client quantity equality converts through the unit's reduction to base units — exactly while the magnitude is an integer and the scale a whole ratio — so 1 m and 100 cm are one member, km/h and m/s are commensurable, and metres never equal seconds; a quantity without a reduction is still compared in its unit as written. The Java membership test expecting 2.0 outside a set holding 2 is corrected to the numeric equality the other clients already implement.

Co-Authored-By: jason.han <[email protected]>
…terals by id

Client value equality now matches the engine's: a MeasurementRef is one reduction at one scale however it is spelt or which declaration names it (SI::'m/s' is m/s, km/m is m/mm), except that a named unit of dimension one reduces to nothing and so is only its own declaration (rad is not sr); an EnumLiteral is its literal id alone, whatever enumeration id or display name accompanies it. Applies to Go, Python, Node, Rust and Java, with the set-membership consequences: equivalent references are one member and a set listing both is refused. The Python duplicate-set test asserts the ValueError raised at construction, where the check now lives.

Co-Authored-By: jason.han <[email protected]>
devin-ai-integration Bot and others added 10 commits September 8, 2026 21:13
…instance refs and arrays

An unresolved instance reference decoded to a plain int, so a set holding instance 1 and Integer 1 was rejected as a duplicate; Array equality compared elements with Python ==, so nested true and 1 collided. Unresolved references are now an InstanceRef (an int subclass sent back as instance_id), and Array compares its elements as same_value does.

Co-Authored-By: jason.han <[email protected]>
InstanceRef is no longer an int subclass, so Vector, Quantity, Array and TensorQuantity dimensions and typed.as_int/as_float/as_complex refuse it instead of encoding it as int_value; Connection still sends it as instance_id.

Co-Authored-By: jason.han <[email protected]>
…et apart from its sequence as a member

canonicalCompare falls through to the calc, bound object and enclosing run
when two function values render alike, so equal sets of functions enumerate
and cross the wire in one order however they were written.

Set membership no longer equates a set with the sequence of its members:
valueEqual and valueKeyFunc keep the two kinds apart, matching every client,
while the == operator and binding agreement still read a set flowing into an
ordered context as its canonical sequence (equalValues).

Co-Authored-By: jason.han <[email protected]>
…sendable members

canonicalCompare orders each value by the representative valueEqual reads it as, so a
real-axis complex number sits among the numbers and an empty collection with null, and
equal sets holding different representatives enumerate alike; a for loop over a set
visits that same canonical order rather than a rendering sort. A set holding a member
with no wire form, or one the service withholds, crosses as one unsupported null naming
the set instead of nulls in the members' places, which would read back as a repeated
member.

Co-Authored-By: jason.han <[email protected]>
…refuse a quantity without a magnitude

Set membership in the Go, Python, Node, Java and Rust clients now judges a
null, an empty sequence and an empty set as one value, as the engine does,
so a set spelling the absent value twice is refused before it is sent or
when it is decoded. The Go client refuses a Quantity carrying no magnitude
with CodeInvalidArgument wherever it is sent (scalar, vector or tensor
component) instead of putting an empty quantity on the wire.

Co-Authored-By: jason.han <[email protected]>
@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/set-and-tensor-values branch from c52bc13 to f00ea98 Compare September 8, 2026 21:19
devin-ai-integration[bot]

This comment was marked as resolved.

A null, an empty Sequence and an empty SetValue compare equal under
sameValue, so valueHash now hashes them to one code: sets that differ
only in how their absent member is spelt hash and key alike.

Co-Authored-By: jason.han <[email protected]>
@HuiJun
HuiJun merged commit 877d429 into main Sep 8, 2026
12 checks passed
@HuiJun
HuiJun deleted the feature/set-and-tensor-values branch September 8, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant